﻿//Copyright (c) 2011 Shane Baker
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:

//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.

//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace XRGLib
{
    /// <summary>
    /// Represents the interface to the console, which holds the root surface and contains methods to create offscreen
    /// surfaces.
    /// </summary>
    /// <remarks>
    /// Normally only a single Console instance will be needed.  A Console instance is responsible for rendering the
    /// root surface to a RenderTarget2D, which can then be drawn to the screen using the appropriate XNA methods.
    /// A Console instance keeps its own SpriteBatch object which it uses to draw to the render target inside
    /// the Flush method, so beware of nesting SpriteBatch calls.  Typically, the order of rendering in Game.Draw would be:
    /// <code>
    /// // Draw some stuff to Console.Root
    /// RenderTarget2D rt = Console.Flush();
    /// spriteBatch.Begin();
    /// spriteBatch.Draw(rt, Vector2.Zero, Color.White);
    /// spriteBatch.End();
    /// </code>
    /// <para/>Internally, the Console and RootSurface keep track of dirty cells, so cells which haven't changed
    /// are not re-drawn during Flush.  This potentially cuts down on the number of draw calls batched during rendering.
    /// </remarks>
    public class Console
    {
        #region CTOR
        /// <summary>
        /// Create the console given a GraphicsDevice and the size of the root surface
        /// </summary>
        /// <remarks>
        /// The create a Console instance, you must pass a valid GraphicsDevice, Font, and size, or an exception
        /// will be thrown.  After construction, a Console will have a valid Root surface to draw to.
        /// </remarks>
        /// <param name="device"></param>
        /// <param name="widthInChars">Width in number of characters</param>
        /// <param name="heightInChars">Height in number of characters</param>
        public Console(GraphicsDevice device, Font font, int widthInChars, int heightInChars)
        {
            if (device == null)
                throw new ArgumentNullException("device");
            if (font == null)
                throw new ArgumentNullException("font");
            if (widthInChars <= 0)
                throw new ArgumentOutOfRangeException("widthInChars");
            if (heightInChars <= 0)
                throw new ArgumentOutOfRangeException("heightInChars");

            this.graphics = device;

            this.font = font;

            spriteBatch = new SpriteBatch(device);

            renderTarget = new RenderTarget2D(device,
                font.CharacterWidth * widthInChars,
                font.CharacterHeight * heightInChars,
                false,
                SurfaceFormat.Color,
                DepthFormat.None,
                0,
                RenderTargetUsage.PreserveContents);

            rootSurface = new RootSurface(widthInChars, heightInChars, font, this);

            //RootClear();
            rootSurface.Clear();
        }
        #endregion

        #region Public

        /// <summary>
        /// The main drawing surface for the console.
        /// </summary>
        public RootSurface Root 
        { 
            get 
            { 
                return this.rootSurface; 
            } 
        }

        /// <summary>
        /// The width in pixels of the current font's characters
        /// </summary>
        public int CharacterWidth
        {
            get
            {
                if (font == null)
                    return 0;
                return font.CharacterWidth;
            }
        }

        /// <summary>
        /// The height in pixels of the current font's characters
        /// </summary>
        public int CharacterHeight
        {
            get
            {
                if (font == null)
                    return 0;
                return font.CharacterHeight;
            }
        }

        /// <summary>
        /// Sets a new font for the console.  The RenderTarget2D size will be set according to the root surface size
        /// and the size of the font characters.  Any changes to window size or resolution need to be handled elsewhere.
        /// </summary>
        /// <param name="font"></param>
        public void ChangeFont(Font font)
        {
            if (font == null)
                throw new ArgumentNullException("font");

            if (System.Object.ReferenceEquals(this.font, font))
                return;

            this.font = font;

            renderTarget = new RenderTarget2D(graphics,
                font.CharacterWidth * rootSurface.Width,
                font.CharacterHeight * rootSurface.Height,
                false,
                SurfaceFormat.Color,
                DepthFormat.None,
                0,
                RenderTargetUsage.PreserveContents);

            rootSurface.font = font;

            rootSurface.Clear();

        }

        /// <summary>
        /// Renders the root console to a RenderTarget2D.
        /// </summary>
        /// <returns>The render target with the root console rendered to it.</returns>
        public RenderTarget2D Flush()
        {
            graphics.SetRenderTarget(renderTarget);

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);

            Rectangle src = new Rectangle(0, 0, 8, 8);

            for (int y = 0; y < rootSurface.Height; y++)
            {
                for (int x = 0; x < rootSurface.Width; x++)
                {
                    if (rootSurface.dirtyCells[x + y * rootSurface.Width] == true)
                    {
                        // background
                        font.SetFontSourceRect(ref src, Font.SolidChar);
                        spriteBatch.Draw(font.texture, new Vector2(x * font.CharacterWidth, y * font.CharacterHeight),
                            src, rootSurface.cells[x + y * rootSurface.Width].back);

                        //foreground
                        font.SetFontSourceRect(ref src, rootSurface.cells[x + y * rootSurface.Width].c);
                        spriteBatch.Draw(font.texture, new Vector2(x * font.CharacterWidth, y * font.CharacterHeight),
                            src, rootSurface.cells[x + y * rootSurface.Width].fore);
                    }
                }
            }

            spriteBatch.End();

            graphics.SetRenderTarget(null);

            Array.Clear(rootSurface.dirtyCells, 0, rootSurface.dirtyCells.Length);

            return renderTarget;
        }

        /// <summary>
        /// Creates an offscreen surface with the provided dimensions.
        /// </summary>
        /// <remarks>
        /// An offscreen surface can be drawn to exactly like the root surface, then blitted to the root surface
        /// with optional simulated alpha blending.  
        /// </remarks>
        /// <param name="width">The width of the surface in number of characters</param>
        /// <param name="height">The height of the surface in number of characters</param>
        /// <returns>The created surface</returns>
        public Surface CreateSurface(int width, int height)
        {
            if (width <= 0)
                throw new ArgumentOutOfRangeException("width");
            if (height <= 0)
                throw new ArgumentOutOfRangeException("height");

            return new Surface(width, height, font, this);
        }

        /// <summary>
        /// Copies a portion of the source surface to the destination surface.
        /// Areas wich would fall outside the destination surface are clipped.
        /// </summary>
        /// <param name="src">The source of the blit, normally an off-screen surface (but can be the root surface)</param>
        /// <param name="dest">The destination surface, often the root surface (but can be an off-screen surface)</param>
        /// <param name="srcRect">The area of the source surface that will be copied</param>
        /// <param name="destX">The destination X coordinate (in character coordinates)</param>
        /// <param name="destY">The destination Y coordinate (in character coordinates)</param>
        public void Blit(Surface src, Surface dest, Rectangle srcRect, int destX, int destY)
        {
            if (src == null)
                throw new ArgumentNullException("src");
            if (dest == null)
                throw new ArgumentNullException("dest");


            Rectangle blitRect = new Rectangle(destX, destY, srcRect.Width, srcRect.Height);
            int deltax = srcRect.Left - blitRect.Left;
            int deltay = srcRect.Top - blitRect.Top;

            bool dstIsRoot = dest is RootSurface;
            RootSurface dstAsRoot = dest as RootSurface;

            blitRect = Rectangle.Intersect(blitRect, new Rectangle(0, 0, dest.Width, dest.Height));

            for (int y = blitRect.Top; y < blitRect.Bottom; y++)
            {
                for (int x = blitRect.Left; x < blitRect.Right; x++)
                {
                    int sx = deltax + x;
                    int sy = deltay + y;

                    dest.cells[x + y * dest.Width].back = src.cells[sx + sy * src.Width].back;
                    dest.cells[x + y * dest.Width].fore = src.cells[sx + sy * src.Width].fore;
                    dest.cells[x + y * dest.Width].c = src.cells[sx + sy * src.Width].c;

                    if (dstIsRoot)
                    {
                        dstAsRoot.dirtyCells[x + y * dest.Width] = true;
                    }
                }
            }
        }

        /// <summary>
        /// Copies a portion of the source surface to the destination surface using simulated alpha transparency.
        /// Areas wich would fall outside the destination surface are clipped.
        /// </summary>
        /// <param name="src"></param>
        /// <param name="dest"></param>
        /// <param name="srcRect"></param>
        /// <param name="destX"></param>
        /// <param name="destY"></param>
        /// <param name="alpha">Alpha from 0 to 1 inclusive</param>
        public void BlitAlpha(Surface src, Surface dest, Rectangle srcRect, int destX, int destY, float alpha)
        {
            if (src == null)
                throw new ArgumentNullException("src");
            if (dest == null)
                throw new ArgumentNullException("dest");

            alpha = MathHelper.Clamp(alpha, 0f, 1f);

            Rectangle blitRect = new Rectangle(destX, destY, srcRect.Width, srcRect.Height);
            int deltax = srcRect.Left - blitRect.Left;
            int deltay = srcRect.Top - blitRect.Top;

            bool dstIsRoot = dest is RootSurface;
            RootSurface dstAsRoot = dest as RootSurface;

            blitRect = Rectangle.Intersect(blitRect, new Rectangle(0, 0, dest.Width, dest.Height));
            Color backCol, foreCol;
            char ch;

            for (int y = blitRect.Top; y < blitRect.Bottom; y++)
            {
                for (int x = blitRect.Left; x < blitRect.Right; x++)
                {
                    int sx = deltax + x;
                    int sy = deltay + y;

                    backCol = Color.Lerp(dest.cells[x + y * dest.Width].back, src.cells[sx + sy * src.Width].back, alpha);
                    if (src.cells[sx + sy * src.Width].c == ' ')
                    {
                        foreCol = Color.Lerp(dest.cells[x + y * dest.Width].fore, src.cells[sx + sy * src.Width].back, alpha);
                        ch = dest.cells[x + y * dest.Width].c;
                    }
                    else
                    {
                        foreCol = src.cells[sx + sy * src.Width].fore;
                        ch = src.cells[sx + sy * src.Width].c;
                    }

                    dest.cells[x + y * dest.Width].back = backCol;
                    dest.cells[x + y * dest.Width].fore = foreCol;
                    dest.cells[x + y * dest.Width].c = ch;

                    if (dstIsRoot)
                    {
                        dstAsRoot.dirtyCells[x + y * dest.Width] = true;
                    }
                }
            }
        }

        /// <summary>
        /// Copies the entire source surface to the destination surface.
        /// Areas wich would fall outside the destination surface are clipped.
        /// </summary>
        /// <param name="src">The source of the blit, normally an off-screen surface (but can be the root surface)</param>
        /// <param name="dest">The destination surface, often the root surface (but can be an off-screen surface)</param>
        /// <param name="destX">The destination X coordinate (in character coordinates)</param>
        /// <param name="destY">The destination Y coordinate (in character coordinates)</param>
        public void Blit(Surface src, Surface dest, int destX, int destY)
        {
            if (src == null)
                throw new ArgumentNullException("src");
            if (dest == null)
                throw new ArgumentNullException("dest");

            Blit(src, dest, new Rectangle(0, 0, src.Width, src.Height), destX, destY);
        }

        /// <summary>
        /// Renders a portion of a texture to the destination surface.  Each pixel of the texture becomes a solid cell of the same
        /// color.
        /// </summary>
        /// <remarks>
        /// Blitting a texture will be less perfomant that blitting a surface, especially for large textures
        /// that will be clipped.
        /// </remarks>
        /// <param name="src">The source of the blit</param>
        /// <param name="dest">The destination surface</param>
        /// <param name="srcRect">The area of the texture that will be copied</param>
        /// <param name="destX">The destination X coordinate (in character coordinates)</param>
        /// <param name="destY">The destination Y coordinate (in character coordinates)</param>
        public void Blit(Texture2D src, Surface dest, Rectangle srcRect, int destX, int destY)
        {
            if (src == null)
                throw new ArgumentNullException("src");
            if (dest == null)
                throw new ArgumentNullException("dest");

            Color[] data = new Color[srcRect.Width * srcRect.Height];

            src.GetData<Color>(0, srcRect, data, 0, data.Length);

            for (int i = 0; i < data.Length; i++)
            {
                int y = i / srcRect.Width;
                int x = i % srcRect.Width;

                dest.PrintChar(destX + x, destY + y, Font.SolidChar, data[i]);
            }
            
        }
        #endregion

        #region private

        Font font;
        RenderTarget2D renderTarget;
        GraphicsDevice graphics;
        SpriteBatch spriteBatch;
        RootSurface rootSurface;

        internal void RootClear()
        {
            
            graphics.SetRenderTarget(renderTarget);
            graphics.Clear(rootSurface.DefaultBackground);
            graphics.SetRenderTarget(null);
        }


        #endregion
    }
}
